using System;
using System.IO;
using System.Collections.Generic;
using System.Xml;
using System.ComponentModel;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using SkinnedModel;

// TODO: replace these with the processor input and output types.
using TInput = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;
//using TOutput = Microsoft.Xna.Framework.Content.Pipeline.Processors.ModelContent;
using TOutput = SkinnedModel.SkinningData;

namespace SkinnedModelPipeline
{
    /// <summary>
    /// This class is patterned after SkinnedModelProcessor, to ensure
    /// compatibility. However this only returns processed animation
    /// clip and is not limited by MaxBones. Since the output is designed
    /// to be a generic animation data, further processing at runtime
    /// is needed to import clip data that is supported by specific
    /// SkinnedModel (i.e. choose only the bones that are present in
    /// the target model).
    /// </summary>
    [ContentProcessor(DisplayName = "AnimationProcessor")]
    public class AnimationProcessor : ContentProcessor<TInput, TOutput>//ModelProcessor
    {
        #region Fields
        public static readonly int DefaultAnimationFramerate = 60;

        private struct SplitTask
        {
            public string Name;
            public float StartTimeSeconds;
            public float EndTimeSeconds;

            public SplitTask(string name, float startTime, float endTime)
            {
                Name = name;
                StartTimeSeconds = startTime;
                EndTimeSeconds = endTime;
            }

            public override string ToString()
            {
                return string.Format("{0} [{1} : {2}]", Name, StartTimeSeconds, EndTimeSeconds);
            }
        }
        #endregion

        #region Content Properties
        private string splitAnimationFilename;
        [DisplayName("!Split Animation Filename"),
        Description("If set, this file will be used to split the model's animations."),
        DefaultValue(typeof(string), "")]
        public virtual string SplitAnimationFilename
        {
            get { return splitAnimationFilename; }
            set { splitAnimationFilename = value; }
        }
        #endregion

        public override TOutput Process(TInput input, ContentProcessorContext context)
        {
            SkinnedModelProcessor.ValidateMesh(input, context, null);

            // Find the skeleton.
            BoneContent skeleton = MeshHelper.FindSkeleton(input);

            if (skeleton == null)
                throw new InvalidContentException("Input skeleton not found.");

            // We don't want to have to worry about different parts of the model being
            // in different local coordinate systems, so let's just bake everything.
            SkinnedModelProcessor.FlattenTransforms(input, skeleton);

            // Read the bind pose and skeleton hierarchy data.
            IList<BoneContent> bones = MeshHelper.FlattenSkeleton(skeleton);

            if (bones.Count > SkinnedEffect.MaxBones)
            {
                // NB. We are not going to display this, so ignore MaxBones
                //throw new InvalidContentException(string.Format(
                //    "Skeleton has {0} bones, but the maximum supported is {1}.",
                //    bones.Count, SkinnedEffect.MaxBones));
            }

            List<Matrix> bindPose = new List<Matrix>();
            List<Matrix> inverseBindPose = new List<Matrix>();
            List<int> skeletonHierarchy = new List<int>();
            Dictionary<string, int> boneIndices = new Dictionary<string, int>();

            foreach (BoneContent bone in bones)
            {
                bindPose.Add(bone.Transform);
                inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform));
                skeletonHierarchy.Add(bones.IndexOf(bone.Parent as BoneContent));
                boneIndices.Add(bone.Name, boneIndices.Count);
            }

            // Convert animation data to our runtime format.
            Dictionary<string, AnimationClip> animationClips;
            animationClips = SkinnedModelProcessor.ProcessAnimations(skeleton.Animations, bones);

            // split animations if necessary
            if (!string.IsNullOrEmpty(splitAnimationFilename))
                SplitAnimations(animationClips, context);

            // Chain to the base ModelProcessor class so it can convert the model data.
            //ModelContent model = base.Process(input, context);
            OpaqueDataDictionary odd = new OpaqueDataDictionary();
            ModelContent model = context.Convert<TInput, ModelContent>(input, "ModelProcessor", odd);

            // Store our custom animation data in the Tag property of the model.
            model.Tag = new SkinningData(animationClips, bindPose,
                                         inverseBindPose, skeletonHierarchy,
                                         boneIndices);

            return model.Tag as SkinningData;
        }

        #region SplitAnimation private methods
        /// <summary>
        /// Splits the animation based on a given split file
        /// </summary>
        /// <param name="animationClips">The original processed animation clips dictionary</param>
        /// <param name="context">The current content processor context</param>
        private void SplitAnimations(Dictionary<string, AnimationClip> animationClips,
                                    ContentProcessorContext context)
        {
            // Verify existence of split file
            string xmlPath = Path.GetFullPath(splitAnimationFilename);

            // Read the XML document
            XmlDocument xmlDocument;
            try
            {
                if (!File.Exists(xmlPath))
                    throw new FileNotFoundException();

                xmlDocument = new XmlDocument();
                xmlDocument.Load(xmlPath);
            }
            catch (FileNotFoundException)
            {
                throw new InvalidContentException("Missing animation split file: " +
                    xmlPath);
            }
            catch (XmlException e)
            {
                throw new InvalidContentException(
                    String.Format("Error parsing animation split file: {0}{1}{2}",
                    xmlPath, Environment.NewLine, e));
            }

            // Find the ANIMATIONS root tag
            XmlElement animationsElement = null;
            foreach (XmlNode xmlNode in xmlDocument.ChildNodes)
            {
                XmlElement xmlElement = xmlNode as XmlElement;
                if (xmlElement != null && xmlElement.Name.ToLower() == "animations")
                {
                    animationsElement = xmlElement;
                    break;
                }
            }

            if (animationsElement == null)
            {
                context.Logger.LogWarning(null, null, "Split animation document does not " +
                "contain an <ANIMATIONS> tag and will be skipped.");
                return;
            }

            // Parse each ANIMATION tag
            bool containsAnimationElement = false;
            foreach (XmlNode animationNode in animationsElement.ChildNodes)
            {
                XmlElement animationElement = animationNode as XmlElement;
                if (animationElement != null && animationElement.Name.ToLower() == "animation")
                {
                    containsAnimationElement = true;

                    string animationName = ParseChildElementInnerText(
                        animationElement, "name");
                    string animationFramerateText = ParseChildElementInnerText(
                        animationElement, "framerate");

                    if (animationName == null)
                    {
                        throw new InvalidContentException("Split animation document contains " +
                            "an animation that does not have a <NAME> tag.");
                    }

                    int animationFramerate = DefaultAnimationFramerate;
                    if (animationFramerateText == null)
                    {
                        context.Logger.LogWarning(null, null, "Using the default {0} " +
                            "frames per second framerate to split animations.");
                    }
                    else
                    {
                        try
                        {
                            animationFramerate = int.Parse(animationFramerateText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Animation {0} has an invalid framerate.",
                                animationName));
                        }
                    }

                    // For now, just add an empty anim clip
                    AnimationClip animationClip;
                    if (animationClips.TryGetValue(animationName, out animationClip))
                    {
                        // Get the list of split animations
                        List<SplitTask> splitAnimationTaskList =
                            ParseSplitAnimationTaskList(animationElement, animationFramerate,
                            animationClip.Duration, context);

                        // Get the new animation clips
                        List<KeyValuePair<string, AnimationClip>> keySplitAnimationClips =
                            SplitAnimation(animationClip, splitAnimationTaskList, context);

                        // Add each animation clip to the dictionary
                        foreach (KeyValuePair<string, AnimationClip> keySplitAnimationClip
                            in keySplitAnimationClips)
                            animationClips.Add(keySplitAnimationClip.Key, keySplitAnimationClip.Value);
                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                                "Input model does not have animation {0}.",
                                animationName));
                    }
                }
            }

            if (!containsAnimationElement)
            {
                context.Logger.LogWarning(null, null, "Split animation document does not contain " +
                    "any <ANIMATION> tag.");
            }
        }

        private string ParseChildElementInnerText(XmlElement element, string childName)
        {
            foreach (XmlNode childNode in element)
            {
                XmlElement childElement = childNode as XmlElement;
                if (childElement != null && childElement.Name.ToLower() == childName)
                {
                    return childElement.InnerText;
                }
            }

            return null;
        }

        private List<SplitTask> ParseSplitAnimationTaskList(XmlElement animationElement,
            int animationFramerate, TimeSpan animationDuration, ContentProcessorContext context)
        {
            List<SplitTask> splitAnimations = new List<SplitTask>();
            List<string> splitAnimationNames = new List<string>();

            string animationName = ParseChildElementInnerText(animationElement, "name");
            foreach (XmlNode childNode in animationElement.ChildNodes)
            {
                XmlElement childElement = childNode as XmlElement;
                if (childElement != null && childElement.Name.ToLower() == "splittask")
                {
                    // Try to get the inner text of all available tags
                    string splitName = ParseChildElementInnerText(childElement, "name");
                    string startTimeText = ParseChildElementInnerText(
                        childElement, "starttimeseconds");
                    string endTimeText = ParseChildElementInnerText(
                        childElement, "endtimeseconds");
                    string startFrameText = ParseChildElementInnerText(
                        childElement, "startframe");
                    string endFrameText = ParseChildElementInnerText(
                        childElement, "endframe");

                    // Check if the split animation has a name
                    if (splitName == null)
                    {
                        throw new InvalidContentException(string.Format(
                            "An split animation on animation {0} does not have tag.",
                                animationName));
                    }

                    // Check if the user have defined both start time and frame
                    if (startTimeText != null && startFrameText != null)
                    {
                        context.Logger.LogWarning(null, null, string.Format(
                            "Split animation {0} on animation {1} has both start time and " +
                            "start frame tags. Start frame will be discarded.",
                            splitName, animationName));
                    }

                    // Check if the user have defined both end time and frame
                    if (endTimeText != null && endFrameText != null)
                    {
                        context.Logger.LogWarning(null, null, string.Format(
                            "Split animation {0} on animation {1} has both end time and " +
                            "end frame tags. End frame will be discarded.",
                            splitName, animationName));
                    }

                    // Parse start time tag
                    float startTime;
                    if (startTimeText != null)
                    {
                        try
                        {
                            startTime = float.Parse(startTimeText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid start time.",
                                splitName, animationName));
                        }
                    }
                    else if (startFrameText != null)
                    {
                        try
                        {
                            int startFrame = int.Parse(startFrameText);
                            startTime = startFrame / (float)animationFramerate;
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid start time.",
                                splitName, animationName));
                        }
                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have a start time tag.",
                            splitName, animationName));
                    }

                    // Parse end time tag
                    float endTime;
                    if (endTimeText != null)
                    {
                        try
                        {
                            endTime = float.Parse(endTimeText);
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid end time.",
                                splitName, animationName));
                        }
                    }
                    else if (endFrameText != null)
                    {
                        try
                        {
                            int endFrame = int.Parse(endFrameText);
                            endTime = endFrame / (float)animationFramerate;
                        }
                        catch (Exception)
                        {
                            throw new InvalidContentException(string.Format(
                                "Split animation {0} on animation {1} has an invalid end time.",
                                splitName, animationName));
                        }
                    }
                    else
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have an end time tag.",
                            splitName, animationName));
                    }

                    // Validate split animation name
                    if (splitAnimationNames.Contains(splitName))
                    {
                        throw new InvalidContentException(string.Format(
                            "Animation {0} contains two split animations named {1}.",
                            animationName, splitName));
                    }
                    splitAnimationNames.Add(splitName);

                    // Validate split animation duration
                    if (endTime <= startTime)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} has a duration less than or " +
                            "equals to zero.", splitName, animationName));
                    }

                    // Validate split animation start time
                    if (startTime < 0)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} does not have a positive " +
                            "start time.", splitName, animationName));
                    }

                    // Validate split animation end time
                    if (endTime > (float)animationDuration.TotalSeconds)
                    {
                        throw new InvalidContentException(string.Format(
                            "Split animation {0} on animation {1} has an end time bigger than " +
                            "the original animation duration.", splitName, animationName));
                    }

                    splitAnimations.Add(new SplitTask(splitName, startTime, endTime));
                }
            }

            return splitAnimations;
        }


        private List<KeyValuePair<string, AnimationClip>> SplitAnimation(
            AnimationClip animationClip, List<SplitTask> splitAnimationTaskList,
            ContentProcessorContext context)
        {
            List<KeyValuePair<string, AnimationClip>> keyAnimationClipList =
                new List<KeyValuePair<string, AnimationClip>>(splitAnimationTaskList.Count);

            foreach (SplitTask splitAnimationTask in splitAnimationTaskList)
            {
                List<Keyframe> splitKeyframes = new List<Keyframe>();
                List<Channels> splitChannels = new List<Channels>();

                TimeSpan splitDuration = TimeSpan.Zero;
                TimeSpan? channelStartTime = null;

                foreach (Keyframe kf in animationClip.Keyframes)
                {
                    if ((kf.Time.TotalSeconds >= splitAnimationTask.StartTimeSeconds) &&
                        (kf.Time.TotalSeconds <= splitAnimationTask.EndTimeSeconds))
                    {
                        // Get the time of the first keyframe found
                        if (channelStartTime == null)
                            channelStartTime = kf.Time;
                        // Add keyframe
                        TimeSpan newKfTime = kf.Time - channelStartTime.Value;
                        splitKeyframes.Add(new Keyframe(kf.Bone, newKfTime, kf.Transform));

                        if (newKfTime > splitDuration)
                            splitDuration = newKfTime;
                    }
                }

                if (splitKeyframes.Count > 0)
                {
                    // Generate the channels list
                    SkinnedModelProcessor.GenerateChannelsList(splitKeyframes, splitChannels);

                    // Add to clip list
                    AnimationClip splitClip = new AnimationClip(splitDuration, splitKeyframes, splitChannels);
                    keyAnimationClipList.Add(new KeyValuePair<string, AnimationClip>(splitAnimationTask.Name, splitClip));
                }
            }
            return keyAnimationClipList;
        }
        #endregion
    }
}